Skip to main content

Headless C2 workflow (Docker, no AppImage)

Headless C2 workflow (Docker, no AppImage)

This guide is for running the packaged backend (kikobot-backend from the .deb) inside Docker, controlling the arm without RViz or Kikobot Studio, and executing trajectories with move_joints.py.


Two different “ports” (do not confuse them)​

WhatTypical valuePurpose
Robot PC IPe.g. 192.168.1.100Your laptop/container reaches the backend on this address
WebSocket port8080JSON API: status, firmware start, motion (optional if you only use ROS CLI)
USB serial device/dev/ttyUSB0 or /dev/ttyACM0Physical arm — UART to servos (not an IP)
  • “Connect using IP/port” for software → ws://<ROBOT_PC_IP>:8080 until you get a connected message (see step 1B).
  • “Connect the arm” for hardware → USB cable + correct /dev/tty… + start_firmware (see step 1A).

Overview (your intended workflow)​

[1A] USB arm visible in container
[1B] Optional: WebSocket handshake on ws://IP:8080 → “connected”
[2] Clean old processes → KIKOBOT_HEADLESS=1 → kikobot-backend c2 → port 8080 listening
[3] Start firmware (real arm only)
[4] Same ROS domain as backend (shipped .deb: **0**) → move_joints.py run waypoint_2.json

ROS domain: The shipped backend does not use domain 33. If ROS_DOMAIN_ID is unset, everything is on domain 0 — same as move_joints.py’s default. Do not export ROS_DOMAIN_ID=33 in only one terminal.


Step 0 — One-time: Docker + .deb​

  • Image with ROS 2 Humble and kikobot-backend installed (/opt/kikobot, kikobot-backend on PATH).
  • For a real arm, run the container with the serial device, for example:
docker run -it --device=/dev/ttyUSB0:/dev/ttyUSB0 ... ros2-humble-workspace
# or docker compose with devices: ["/dev/ttyUSB0:/dev/ttyUSB0"]

Inside the container, confirm the device:

ls -l /dev/ttyUSB* /dev/ttyACM* 2>/dev/null

Step 1 — Connect the arm (until successful)​

  1. Power the C2 controller and plug USB into the same machine that runs Docker (or pass the device into the container as above).
  2. Find the port:
ls -l /dev/serial/by-id/ 2>/dev/null
# or: python3 /opt/kikobot/share/c2/kikobot_uart_with_gripper_loop.py --list-ports
  1. You will use this path in step 3 as PORT (e.g. /dev/ttyUSB0).

Success criteria: device node exists and is readable (test -r /dev/ttyUSB0).

Use this to confirm the backend is reachable before motion. From your integration PC or a second shell in the container:

pip install 'websockets>=12'   # once

# Same machine as backend:
python3 /opt/kikobot/share/examples/kikobot_ws_client_example.py --host 127.0.0.1 status

# Remote machine (robot PC IP):
python3 kikobot_ws_client_example.py --host 192.168.1.100 status

Success criteria: JSON with "type": "connected" and robot_profile: "c2", then firmware_status / pong without connection errors.

If this fails, fix step 2 before running waypoints.

Note: If the backend is only inside Docker and you connect from the host, publish port 8080 (-p 8080:8080) or use host network mode, and use 127.0.0.1 or the container/host IP consistently.


Step 2 — Start Docker, then headless backend​

2A — Clean up a previous run (required in Docker)​

Avoid address already in use on 8080:

pkill -f websocket_bridge.py 2>/dev/null || true
pkill -f MainActionServer 2>/dev/null || true
pkill -f 'ros2 launch urdf_v3' 2>/dev/null || true
sleep 2
rm -f /tmp/kikobot_backend_c2.lock
fuser -k 8080/tcp 2>/dev/null || true
ss -ltn | grep 8080 || echo "8080 is free"

2B — Start backend (headless)​

In one dedicated terminal (keep it open):

source /opt/ros/humble/setup.bash
source /opt/kikobot/ros/install/setup.bash

export KIKOBOT_HEADLESS=1
# Shipped .deb: ROS_DOMAIN_ID is usually unset → DDS domain **0** (ROS 2 default).
# Only set ROS_DOMAIN_ID if you set the same value in every shell (backend + move_joints).

kikobot-backend c2

Success criteria:

  • Log line: WebSocket bridge listening on port 8080
  • No websocket_bridge exited during startup
  • Optional: ss -ltn | grep 8080

Headless means no RViz; the Simulation node and motion planner still run so trajectories can execute in sim (or on hardware after firmware).


Step 3 — Start firmware (real arm only)​

Skip in pure simulation if you only need RViz-less sim motion.

With backend running, in a second shell:

Option A — WebSocket (no AppImage):

python3 -c "
import asyncio, json, websockets
async def main():
async with websockets.connect('ws://127.0.0.1:8080') as ws:
print(await ws.recv()) # connected
await ws.send(json.dumps({'type':'start_firmware','port':'/dev/ttyUSB0'}))
print(await ws.recv())
asyncio.run(main())
"

Option B — Run firmware script directly (if your image documents it):

cd /opt/kikobot/share/c2
python3 kikobot_uart_with_gripper_loop.py -p /dev/ttyUSB0

Success criteria: firmware_status → running: true, or firmware log without immediate exit (/tmp/kikobot_firmware.log).


Step 4 — Run waypoint trajectory (move_joints.py)​

Use a second docker exec shell. Same container as the backend.

source /opt/ros/humble/setup.bash
source /opt/kikobot/ros/install/setup.bash

# Must match the backend. Shipped backend uses domain **0** unless you exported ROS_DOMAIN_ID.
# Quick check in the backend terminal: echo "$ROS_DOMAIN_ID" (empty = 0)
export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-0}"

# If move_joints.py is on your workspace mount:
cd /workspace # or wherever waypoint_2.json lives

python3 move_joints.py joints # quick check: live joint state
python3 move_joints.py run waypoint_2.json --sync

--sync: aligns the planner with the first pose before a long sequence (recommended).

Success criteria: [OK] motion complete per segment; in sim, joint values change (check ros2 topic echo /joint_states --once).

If move_joints.py is not in the container​

Copy from your dev tree into the container mount, or call motion via WebSocket motion_plan (see WEBSOCKET_INTEGRATION_GUIDE.pdf).


Best practices checklist​

#Practice
1One kikobot-backend c2 per container; stop with Ctrl+C, not by closing the terminal only
2Run cleanup (2A) before every fresh backend start in long-lived Docker containers
3ROS_DOMAIN_ID must match backend and move_joints (shipped .deb → 0 / unset)
4Distinguish 8080 (TCP/WebSocket) from /dev/ttyUSB0 (arm)
5Verify 8080 with kikobot_ws_client_example.py status before blaming waypoints
6Real arm: firmware running before expecting hardware motion
7Waypoint JSON must respect C2 joint limits; do not reuse files from other robot models
8Logs: /tmp/kikobot_websocket.log, /tmp/kikobot_c2_display.log

Quick reference — environment variables​

VariableHeadless Docker suggestion
KIKOBOT_HEADLESS=1No RViz
ROS_DOMAIN_IDOmit or 0 for shipped .deb; set the same value in backend + move_joints if you override
KIKOBOT_ROBOT_MODEL=c2Set by kikobot-backend c2

Troubleshooting​

SymptomAction
8080 address already in useStep 2A cleanup
context is invalid on WS motionRestart backend after cleanup
move_joints cannot see joints / action serverWrong ROS_DOMAIN_ID; backend not running
Motion only worked with RViz beforeOld bridge/planner zombie; headless is fine after clean start
Arm does not move on hardwareFirmware not started; USB not in container; wrong tty

  • WEBSOCKET_INTEGRATION_GUIDE.pdf — full WebSocket API (ws://IP:8080)
  • /opt/kikobot/CLIENT_INSTALL.txt — install .deb
  • examples/kikobot_ws_client_example.py — connection test client